// Copyright (c) 2023 CHANGLEI. All rights reserved.

import 'dart:collection';

import 'package:dio/dio.dart';
import 'package:flutter_grasp/src/gio/entry_stub.dart'
    if (dart.library.html) 'entry/gio_for_browser.dart'
    if (dart.library.io) 'entry/gio_for_native.dart';

const _kValidStatus = 200;
const _kValidCode = 200;
const _kTimeout = Duration(milliseconds: 90 * 1000);
const _kCodeKey = 'code';
const _kMessageKey = 'message';
const _kDataKey = 'data';

/// 返回有效的code，比如：200
typedef ValidateCode = bool Function(int code);

/// [GioError] describes the exception info when a request failed.
/// Deprecated in favor of [GioException] and will be removed in future major versions.
@Deprecated('Use GioException instead. This will be removed in 6.0.0')
typedef GioError = GioException;

/// Mixin that overrides mutating map operations with implementations that
/// throw.
mixin _UnmodifiableMapMixin<K, V> implements Map<K, V> {
  /// This operation is not supported by an unmodifiable map.
  @override
  void operator []=(K key, V value) {
    throw UnsupportedError('Cannot modify unmodifiable map');
  }

  /// This operation is not supported by an unmodifiable map.
  @override
  void addAll(Map<K, V> other) {
    throw UnsupportedError('Cannot modify unmodifiable map');
  }

  /// This operation is not supported by an unmodifiable map.
  @override
  void addEntries(Iterable<MapEntry<K, V>> entries) {
    throw UnsupportedError('Cannot modify unmodifiable map');
  }

  /// This operation is not supported by an unmodifiable map.
  @override
  void clear() {
    throw UnsupportedError('Cannot modify unmodifiable map');
  }

  /// This operation is not supported by an unmodifiable map.
  @override
  V? remove(Object? key) {
    throw UnsupportedError('Cannot modify unmodifiable map');
  }

  /// This operation is not supported by an unmodifiable map.
  @override
  void removeWhere(bool Function(K key, V value) test) {
    throw UnsupportedError('Cannot modify unmodifiable map');
  }

  /// This operation is not supported by an unmodifiable map.
  @override
  V putIfAbsent(K key, V Function() ifAbsent) {
    throw UnsupportedError('Cannot modify unmodifiable map');
  }

  /// This operation is not supported by an unmodifiable map.
  @override
  V update(K key, V Function(V value) update, {V Function()? ifAbsent}) {
    throw UnsupportedError('Cannot modify unmodifiable map');
  }

  /// This operation is not supported by an unmodifiable map.
  @override
  void updateAll(V Function(K key, V value) update) {
    throw UnsupportedError('Cannot modify unmodifiable map');
  }
}

typedef _Map = MapBase<String, Object?>;
typedef _UnmodifiableMap = _UnmodifiableMapMixin<String, Object?>;

extension on DioExceptionType {
  String toPrettyDescription() {
    switch (this) {
      case DioExceptionType.connectionTimeout:
        return 'connection timeout';
      case DioExceptionType.sendTimeout:
        return 'send timeout';
      case DioExceptionType.receiveTimeout:
        return 'receive timeout';
      case DioExceptionType.badCertificate:
        return 'bad certificate';
      case DioExceptionType.badResponse:
        return 'bad response';
      case DioExceptionType.cancel:
        return 'request cancelled';
      case DioExceptionType.connectionError:
        return 'connection error';
      case DioExceptionType.unknown:
        return 'unknown';
    }
  }
}

/// 自定义Response
final class GioResponse with _Map, _UnmodifiableMap {
  const GioResponse._(
    this.code,
    this.message,
    this.data,
    this._rawData,
  );

  /// 返回的code
  final int code;

  /// 返回的message
  final String message;

  /// 返回的data
  final Object? data;

  /// 原始数据
  final Map<String, Object?> _rawData;

  @override
  String toString() => _rawData.toString();

  @override
  Iterable<String> get keys => _rawData.keys;

  @override
  Object? operator [](Object? key) => _rawData[key];
}

/// 自定义Error
final class GioException extends DioException with _Map, _UnmodifiableMap implements GioResponse {
  GioException._internal({
    required super.requestOptions,
    required super.response,
    required super.stackTrace,
    required super.message,
    required this.code,
    required this.data,
    required Map<String, Object?> rawData,
  }) : _rawData = rawData;

  factory GioException._(
    int code,
    String message,
    Object? data,
    Map<String, Object?> rawData,
    Response<dynamic> response,
  ) {
    return GioException._internal(
      requestOptions: response.requestOptions,
      stackTrace: response.stackTrace,
      response: response,
      code: code,
      data: data,
      message: message,
      rawData: rawData,
    );
  }

  /// 错误码
  @override
  final int code;

  @override
  final Object? data;

  @override
  final Map<String, Object?> _rawData;

  @override
  String get message => super.message ?? error?.toString() ?? '';

  @override
  Response<Object?> get response => super.response as Response<Object?>;

  @override
  DioExceptionType get type => DioExceptionType.badResponse;

  @override
  bool operator ==(Object other) {
    return identical(this, other) || other is GioException && other.code == code;
  }

  @override
  int get hashCode => code.hashCode;

  @override
  Iterable<String> get keys => _rawData.keys;

  @override
  Object? operator [](Object? key) => _rawData[key];

  @override
  String toString() {
    String msg = 'GioException [${type.toPrettyDescription()}]: $message';
    if (error != null) {
      msg += '\nError: $error';
    }
    return msg;
  }
}

/// 拦截器，解析服务器返回的数据
final class GioInterceptor extends Interceptor {
  /// 拦截器，解析服务器返回的数据
  const GioInterceptor();

  @override
  void onError(DioException err, ErrorInterceptorHandler handler) {
    handler.next(_assureException(err) ?? err);
  }

  @override
  void onResponse(Response<dynamic> response, ResponseInterceptorHandler handler) {
    final gioResponse = _assureResponse(response);
    if (response.responseDataTypeInstanceOf<GioResponse>()) {
      handler.next(response..data = gioResponse);
    } else if (gioResponse is GioException) {
      handler.reject(gioResponse, true);
    } else if (gioResponse == null) {
      handler.next(response);
    } else {
      handler.next(response..data = gioResponse.data);
    }
  }

  static GioException? _assureException(DioException err) {
    if (err is GioException) {
      return err;
    }
    final gioResponse = _assureResponse(err.response);
    return gioResponse is GioException ? gioResponse : null;
  }

  static GioResponse? _assureResponse(Response<dynamic>? response) {
    final rawData = response?.data;
    final code = response?.decodedCode;
    if (response == null || rawData is! Map<String, dynamic> || code == null) {
      return null;
    }
    final message = response.decodedMessage ?? '';
    final data = response.decodedData;
    if (response.validateCode(code)) {
      return GioResponse._(code, message, data, rawData);
    } else {
      return GioException._(code, message, data, rawData, response);
    }
  }
}

/// Created by changlei on 2020/8/26.
///
/// gio，最终通过它调用
// ignore: prefer_mixin
abstract class Gio with DioMixin implements Dio {
  /// 默认构造函数
  factory Gio([GioBaseOptions? options]) {
    return createGio(options);
  }

  /// 创建一个通用的[Gio]
  factory Gio.normal({
    String? method,
    Duration? connectTimeout = _kTimeout,
    Duration? receiveTimeout = _kTimeout,
    Duration? sendTimeout = _kTimeout,
    String baseUrl = '',
    Map<String, dynamic>? queryParameters,
    Map<String, dynamic>? extra,
    Map<String, dynamic>? headers,
    ResponseType? responseType = ResponseType.json,
    String? contentType = Headers.jsonContentType,
    bool? receiveDataWhenStatusError,
    bool? followRedirects,
    int? maxRedirects,
    bool? persistentConnection,
    RequestEncoder? requestEncoder,
    ResponseDecoder? responseDecoder,
    ListFormat? listFormat,
    int validStatus = _kValidStatus,
    int validCode = _kValidCode,
    String codeKey = _kCodeKey,
    String messageKey = _kMessageKey,
    String dataKey = _kDataKey,
  }) {
    return Gio(
      GioBaseOptions(
        method: method,
        connectTimeout: connectTimeout,
        receiveTimeout: receiveTimeout,
        sendTimeout: sendTimeout,
        baseUrl: baseUrl,
        queryParameters: queryParameters,
        extra: extra,
        headers: headers,
        responseType: responseType,
        contentType: contentType,
        receiveDataWhenStatusError: receiveDataWhenStatusError,
        followRedirects: followRedirects,
        maxRedirects: maxRedirects,
        persistentConnection: persistentConnection,
        requestEncoder: requestEncoder,
        responseDecoder: responseDecoder,
        listFormat: listFormat,
        validateStatus: (status) => status == validStatus,
        validateCode: (code) => code == validCode,
        keysOptions: KeysOptions(
          codeKey: codeKey,
          messageKey: messageKey,
          dataKey: dataKey,
        ),
      ),
    );
  }
}

/// 配置key
final class KeysOptions {
  /// 构造函数
  const KeysOptions({
    this.codeKey = _kCodeKey,
    this.messageKey = _kMessageKey,
    this.dataKey = _kDataKey,
  });

  /// code对应的key
  final String codeKey;

  /// message对应的key
  final String messageKey;

  /// data对应的key
  final String dataKey;
}

/// [Gio] options
base class GioOptions extends Options {
  /// [Gio] options
  GioOptions({
    super.method,
    super.sendTimeout,
    super.receiveTimeout,
    super.extra,
    super.headers,
    super.preserveHeaderCase,
    super.responseType,
    super.contentType,
    super.validateStatus,
    super.receiveDataWhenStatusError,
    super.followRedirects,
    super.maxRedirects,
    super.persistentConnection,
    super.requestEncoder,
    super.responseDecoder,
    super.listFormat,
    this.validateCode,
    this.keysOptions,
  });

  /// 验证code是否有效
  final ValidateCode? validateCode;

  /// key配置
  final KeysOptions? keysOptions;

  /// Create a Option from current instance with merging attributes.
  @override
  GioOptions copyWith({
    String? method,
    Duration? sendTimeout,
    Duration? receiveTimeout,
    Map<String, Object?>? extra,
    Map<String, Object?>? headers,
    bool? preserveHeaderCase,
    ResponseType? responseType,
    String? contentType,
    ValidateStatus? validateStatus,
    bool? receiveDataWhenStatusError,
    bool? followRedirects,
    int? maxRedirects,
    bool? persistentConnection,
    RequestEncoder? requestEncoder,
    ResponseDecoder? responseDecoder,
    ListFormat? listFormat,
    ValidateCode? validateCode,
    KeysOptions? keysOptions,
  }) {
    Map<String, dynamic>? effectiveHeaders;
    if (headers == null && this.headers != null) {
      effectiveHeaders = _caseInsensitiveKeyMap(this.headers);
    }

    if (headers != null) {
      headers = _caseInsensitiveKeyMap(headers);
      assert(
        !(contentType != null && headers.containsKey(Headers.contentTypeHeader)),
        'You cannot set both contentType param and a content-type header',
      );
    }

    Map<String, dynamic>? effectiveExtra;
    if (extra == null && this.extra != null) {
      effectiveExtra = Map.from(this.extra!);
    }

    return GioOptions(
      method: method ?? this.method,
      sendTimeout: sendTimeout ?? this.sendTimeout,
      receiveTimeout: receiveTimeout ?? this.receiveTimeout,
      extra: extra ?? effectiveExtra,
      headers: headers ?? effectiveHeaders,
      preserveHeaderCase: preserveHeaderCase ?? this.preserveHeaderCase,
      responseType: responseType ?? this.responseType,
      contentType: contentType ?? this.contentType,
      validateStatus: validateStatus ?? this.validateStatus,
      receiveDataWhenStatusError: receiveDataWhenStatusError ?? this.receiveDataWhenStatusError,
      followRedirects: followRedirects ?? this.followRedirects,
      maxRedirects: maxRedirects ?? this.maxRedirects,
      persistentConnection: persistentConnection ?? this.persistentConnection,
      requestEncoder: requestEncoder ?? this.requestEncoder,
      responseDecoder: responseDecoder ?? this.responseDecoder,
      listFormat: listFormat ?? this.listFormat,
      validateCode: validateCode ?? this.validateCode,
      keysOptions: keysOptions ?? this.keysOptions,
    );
  }

  @override
  GioRequestOptions compose(
    BaseOptions baseOpt,
    String path, {
    Object? data,
    Map<String, dynamic>? queryParameters,
    CancelToken? cancelToken,
    Options? options,
    ProgressCallback? onSendProgress,
    ProgressCallback? onReceiveProgress,
    StackTrace? sourceStackTrace,
  }) {
    final query = <String, dynamic>{};
    query.addAll(baseOpt.queryParameters);
    if (queryParameters != null) {
      query.addAll(queryParameters);
    }

    final headers = _caseInsensitiveKeyMap(baseOpt.headers);
    if (this.headers != null) {
      headers.addAll(this.headers!);
    }
    if (this.contentType != null) {
      headers[Headers.contentTypeHeader] = this.contentType;
    }
    final String? contentType = headers[Headers.contentTypeHeader] as String?;
    final extra = Map<String, dynamic>.from(baseOpt.extra);
    if (this.extra != null) {
      extra.addAll(this.extra!);
    }
    final method = (this.method ?? baseOpt.method).toUpperCase();
    final requestOptions = GioRequestOptions(
      method: method,
      headers: headers,
      extra: extra,
      baseUrl: baseOpt.baseUrl,
      path: path,
      data: data,
      preserveHeaderCase: preserveHeaderCase ?? baseOpt.preserveHeaderCase,
      sourceStackTrace: sourceStackTrace ?? StackTrace.current,
      connectTimeout: baseOpt.connectTimeout,
      sendTimeout: sendTimeout ?? baseOpt.sendTimeout,
      receiveTimeout: receiveTimeout ?? baseOpt.receiveTimeout,
      responseType: responseType ?? baseOpt.responseType,
      validateStatus: validateStatus ?? baseOpt.validateStatus,
      receiveDataWhenStatusError: receiveDataWhenStatusError ?? baseOpt.receiveDataWhenStatusError,
      followRedirects: followRedirects ?? baseOpt.followRedirects,
      maxRedirects: maxRedirects ?? baseOpt.maxRedirects,
      persistentConnection: persistentConnection ?? baseOpt.persistentConnection,
      queryParameters: query,
      requestEncoder: requestEncoder ?? baseOpt.requestEncoder,
      responseDecoder: responseDecoder ?? baseOpt.responseDecoder,
      listFormat: listFormat ?? baseOpt.listFormat,
      onReceiveProgress: onReceiveProgress,
      onSendProgress: onSendProgress,
      cancelToken: cancelToken,
      contentType: contentType ?? this.contentType ?? baseOpt.contentType,
      validateCode: validateCode ?? (baseOpt is GioBaseOptions ? baseOpt.validateCode : null),
      keysOptions: keysOptions ?? (baseOpt is GioBaseOptions ? baseOpt.keysOptions : null),
    );
    requestOptions.cancelToken?.requestOptions = requestOptions;
    return requestOptions;
  }
}

/// [Gio]base options
base class GioBaseOptions extends BaseOptions {
  /// 构造函数
  GioBaseOptions({
    super.method,
    super.connectTimeout,
    super.receiveTimeout,
    super.sendTimeout,
    super.baseUrl,
    super.queryParameters,
    super.extra,
    super.headers,
    super.preserveHeaderCase,
    super.responseType,
    super.contentType,
    super.validateStatus,
    super.receiveDataWhenStatusError,
    super.followRedirects,
    super.maxRedirects,
    super.persistentConnection,
    super.requestEncoder,
    super.responseDecoder,
    super.listFormat,
    this.validateCode,
    this.keysOptions = const KeysOptions(),
  });

  /// 验证code是否有效
  final ValidateCode? validateCode;

  /// key配置
  final KeysOptions keysOptions;

  /// Create a Option from current instance with merging attributes.
  @override
  GioBaseOptions copyWith({
    String? method,
    String? baseUrl,
    Map<String, dynamic>? queryParameters,
    String? path,
    Duration? connectTimeout,
    Duration? receiveTimeout,
    Duration? sendTimeout,
    Map<String, Object?>? extra,
    Map<String, Object?>? headers,
    bool? preserveHeaderCase,
    ResponseType? responseType,
    String? contentType,
    ValidateStatus? validateStatus,
    bool? receiveDataWhenStatusError,
    bool? followRedirects,
    int? maxRedirects,
    bool? persistentConnection,
    RequestEncoder? requestEncoder,
    ResponseDecoder? responseDecoder,
    ListFormat? listFormat,
    ValidateCode? validateCode,
    KeysOptions? keysOptions,
  }) {
    return GioBaseOptions(
      method: method ?? this.method,
      baseUrl: baseUrl ?? this.baseUrl,
      queryParameters: queryParameters ?? this.queryParameters,
      connectTimeout: connectTimeout ?? this.connectTimeout,
      receiveTimeout: receiveTimeout ?? this.receiveTimeout,
      sendTimeout: sendTimeout ?? this.sendTimeout,
      extra: extra ?? Map<String, dynamic>.from(this.extra),
      headers: headers ?? Map<String, dynamic>.from(this.headers),
      preserveHeaderCase: preserveHeaderCase ?? this.preserveHeaderCase,
      responseType: responseType ?? this.responseType,
      contentType: contentType ?? this.contentType,
      validateStatus: validateStatus ?? this.validateStatus,
      receiveDataWhenStatusError: receiveDataWhenStatusError ?? this.receiveDataWhenStatusError,
      followRedirects: followRedirects ?? this.followRedirects,
      maxRedirects: maxRedirects ?? this.maxRedirects,
      persistentConnection: persistentConnection ?? this.persistentConnection,
      requestEncoder: requestEncoder,
      responseDecoder: responseDecoder ?? this.responseDecoder,
      listFormat: listFormat ?? this.listFormat,
      validateCode: validateCode ?? this.validateCode,
      keysOptions: keysOptions ?? this.keysOptions,
    );
  }
}

/// [Gio]request options
base class GioRequestOptions extends RequestOptions {
  /// 构造函数
  GioRequestOptions({
    super.method,
    super.sendTimeout,
    super.receiveTimeout,
    super.connectTimeout,
    super.data,
    super.path,
    super.queryParameters,
    super.onReceiveProgress,
    super.onSendProgress,
    super.cancelToken,
    super.baseUrl,
    super.extra,
    super.headers,
    super.preserveHeaderCase,
    super.responseType,
    super.contentType,
    super.validateStatus,
    super.receiveDataWhenStatusError,
    super.followRedirects,
    super.maxRedirects,
    super.persistentConnection,
    super.requestEncoder,
    super.responseDecoder,
    super.listFormat,
    super.setRequestContentTypeWhenNoPayload,
    super.sourceStackTrace,
    this.validateCode,
    this.keysOptions = const KeysOptions(),
  });

  /// 验证code
  final ValidateCode? validateCode;

  /// keys配置
  final KeysOptions? keysOptions;

  Type? _responseDataType;

  /// Create a Option from current instance with merging attributes.
  @override
  GioRequestOptions copyWith({
    String? method,
    Duration? sendTimeout,
    Duration? receiveTimeout,
    Duration? connectTimeout,
    dynamic data,
    String? path,
    Map<String, dynamic>? queryParameters,
    String? baseUrl,
    ProgressCallback? onReceiveProgress,
    ProgressCallback? onSendProgress,
    CancelToken? cancelToken,
    Map<String, dynamic>? extra,
    Map<String, dynamic>? headers,
    bool? preserveHeaderCase,
    ResponseType? responseType,
    String? contentType,
    ValidateStatus? validateStatus,
    bool? receiveDataWhenStatusError,
    bool? followRedirects,
    int? maxRedirects,
    bool? persistentConnection,
    RequestEncoder? requestEncoder,
    ResponseDecoder? responseDecoder,
    ListFormat? listFormat,
    bool? setRequestContentTypeWhenNoPayload,
    ValidateCode? validateCode,
    KeysOptions? keysOptions,
  }) {
    return GioRequestOptions(
      method: method ?? this.method,
      sendTimeout: sendTimeout ?? this.sendTimeout,
      receiveTimeout: receiveTimeout ?? this.receiveTimeout,
      connectTimeout: connectTimeout ?? this.connectTimeout,
      data: data ?? this.data,
      path: path ?? this.path,
      queryParameters: queryParameters ?? this.queryParameters,
      baseUrl: baseUrl ?? this.baseUrl,
      onReceiveProgress: onReceiveProgress ?? this.onReceiveProgress,
      onSendProgress: onSendProgress ?? this.onSendProgress,
      cancelToken: cancelToken ?? this.cancelToken,
      extra: extra ?? Map<String, dynamic>.from(this.extra),
      headers: headers ?? Map<String, dynamic>.from(this.headers),
      preserveHeaderCase: preserveHeaderCase ?? this.preserveHeaderCase,
      responseType: responseType ?? this.responseType,
      contentType: contentType ?? this.contentType,
      validateStatus: validateStatus ?? this.validateStatus,
      receiveDataWhenStatusError: receiveDataWhenStatusError ?? this.receiveDataWhenStatusError,
      followRedirects: followRedirects ?? this.followRedirects,
      maxRedirects: maxRedirects ?? this.maxRedirects,
      persistentConnection: persistentConnection ?? this.persistentConnection,
      requestEncoder: requestEncoder,
      responseDecoder: responseDecoder ?? this.responseDecoder,
      listFormat: listFormat ?? this.listFormat,
      validateCode: validateCode ?? this.validateCode,
      keysOptions: keysOptions ?? this.keysOptions,
      // ignore: invalid_use_of_internal_member
      sourceStackTrace: sourceStackTrace,
    ).._responseDataType = _responseDataType;
  }
}

/// 设置请求的数据类型
void setResponseDataType<T>(GioRequestOptions options) {
  options._responseDataType = T;
}

extension on Response<dynamic> {
  // ignore: invalid_use_of_internal_member
  StackTrace? get stackTrace => requestOptions.sourceStackTrace;

  GioRequestOptions? get gioRequestOptions {
    final requestOptions = this.requestOptions;
    return requestOptions is GioRequestOptions ? requestOptions : null;
  }

  bool responseDataTypeInstanceOf<T>() => gioRequestOptions?._responseDataType == T;

  bool validateCode(int code) => gioRequestOptions?.validateCode?.call(code) != false;

  KeysOptions get keyOptions => gioRequestOptions?.keysOptions ?? const KeysOptions();

  Map<String, dynamic>? get rawData => data is Map<String, dynamic> ? data as Map<String, dynamic> : null;

  int? get decodedCode => rawData?[keyOptions.codeKey] as int?;

  String? get decodedMessage => rawData?[keyOptions.messageKey] as String?;

  Object? get decodedData => rawData?[keyOptions.dataKey];
}

/// 只读的[GioResponse]
extension ReadOnlyResponse on GioResponse {
  /// See [Map.keys]
  Iterable<String> get keys => _rawData.keys;

  /// See [Map.values]
  Iterable<Object?> get values => _rawData.values;

  /// See [Map.entries]
  Iterable<MapEntry<String, Object?>> get entries => _rawData.entries;

  /// See [Map.containsKey]
  bool containsKey(Object? key) => _rawData.containsKey(key);

  /// See [Map]
  Object? operator [](String key) => _rawData[key];

  /// 转为[Map]
  Map<String, Object?> asMap() => Map.unmodifiable(_rawData);
}

Map<String, V> _caseInsensitiveKeyMap<V>([Map<String, V>? value]) {
  final map = LinkedHashMap<String, V>(
    equals: (key1, key2) => key1.toLowerCase() == key2.toLowerCase(),
    hashCode: (key) => key.toLowerCase().hashCode,
  );
  if (value != null && value.isNotEmpty) {
    map.addAll(value);
  }
  return map;
}
